using System;
using System.IO;
using System.Reflection;
using System.Collections.Generic;
using System.Text;

namespace Open.Data.Persistence
{
    public class ReflectionParser
    {
        //Property variables
        private string _name;
        private string _fullName;
        private bool _isUnderlying;
        private string _underlyingName;
        private Type _underlyingType;
        private Type _keyType;
        private Type _persistType;
        private Type _baseType;

        private bool _hasObjects;
        private bool _isView;
        private bool _hasPrimaryKey;
        private bool _hasCompoundKeys;
        private bool _hasForeignKeys;
        private bool _hasBinaryProperties;

        private int _rank;

        private PropertyInfo _primaryKey;
        private PropertyInfo[] _properties;
        private PropertyInfo[] _foreignKeys;
        private PropertyInfo[] _compoundKeys;
        private PropertyInfo[] _valueProperties;
        private PropertyInfo[] _referenceProperties;
        private PropertyInfo[] _binaryProperties;
        private List<PropertyInfo[]> _indexes;

        private List<IIndexAttribute> _indexAttributes;
        private IPrimaryKeyAttribute _primaryKeyAttribute;
        private List<IReferenceAttribute> _referenceAttributes;

        //Working variables
        private List<PropertyInfo> _propertyList;
        private List<PropertyInfo> _valueList;
        private List<PropertyInfo> _referenceList;
        private List<PropertyInfo> _binaryList;
        private List<PropertyInfo> _compoundList;
        private List<PropertyInfo> _foreignList;

        #region Interface

        public virtual Type[] Types
        {
            get
            {
                return new Type[] { };
            }
        }

        public bool IsUnderlying
        {
            get
            {
                return _isUnderlying;
            }
            set
            {
                _isUnderlying = value;
            }
        }

        public bool IsView
        {
            get
            {
                return _isView;
            }
            set
            {
                _isView = value;
            }
        }

        public bool HasPrimaryKey
        {
            get
            {
                return _hasPrimaryKey;
            }
            set
            {
                _hasPrimaryKey = value;
            }
        }

        public bool HasCompoundKeys
        {
            get
            {
                return _hasCompoundKeys;
            }
            set
            {
                _hasCompoundKeys = value;
            }
        }

        public bool HasForeignKeys
        {
            get
            {
                return _hasForeignKeys;
            }
            set
            {
                _hasForeignKeys = value;
            }
        }

        public bool HasObjects
        {
            get
            {
                return _hasObjects;
            }
            set
            {
                _hasObjects = value;
            }
        }

        public bool HasBinaryProperties
        {
            get
            {
                return _hasBinaryProperties;
            }
            set
            {
                _hasBinaryProperties = value;
            }
        }

        public string Name
        {
            get
            {
                return _name;
            }
            set
            {
                _name = value;
            }
        }

        public string FullName
        {
            get
            {
                return _fullName;
            }
            set
            {
                _fullName = value;
            }
        }

        public string UnderlyingName
        {
            get
            {
                return _underlyingName;
            }
            set
            {
                _underlyingName = value;
            }
        }

        public Type UnderlyingType
        {
            get
            {
                return _underlyingType;
            }
            set
            {
                _underlyingType = value;
            }
        }

        public Type BaseType
        {
            get
            {
                return _baseType;
            }
            set
            {
                _baseType = value;
            }
        }

        public Type KeyType
        {
            get
            {
                return _keyType;
            }
            set
            {
                _keyType = value;
            }
        }

        public Type PersistType
        {
            get
            {
                return _persistType;
            }
            set
            {
                _persistType = value;
            }
        }

        public PropertyInfo PrimaryKey
        {
            get
            {
                return _primaryKey;
            }
            set
            {
                _primaryKey = value;
            }
        }

        public PropertyInfo[] Properties
        {
            get
            {
                return _properties;
            }
            set
            {
                _properties = value;
            }
        }

        public PropertyInfo[] ForeignKeys
        {
            get
            {
                return _foreignKeys;
            }
            set
            {
                _foreignKeys = value;
            }
        }

        public PropertyInfo[] CompoundKeys
        {
            get
            {
                return _compoundKeys;
            }
            set
            {
                _compoundKeys = value;
            }
        }

        public PropertyInfo[] ValueProperties
        {
            get
            {
                return _valueProperties;
            }
            set
            {
                _valueProperties = value;
            }
        }

        public PropertyInfo[] ReferenceProperties
        {
            get
            {
                return _referenceProperties;
            }
            set
            {
                _referenceProperties = value;
            }
        }

        public PropertyInfo[] BinaryProperties
        {
            get
            {
                return _binaryProperties;
            }
            set
            {
                _binaryProperties = value;
            }
        }

        public List<PropertyInfo[]> Indexes
        {
            get
            {
                return _indexes;
            }
            set
            {
                _indexes = value;
            }
        }

        public List<IIndexAttribute> IndexAttributes
        {
            get
            {
                return _indexAttributes;
            }
            set
            {
                _indexAttributes = value;
            }
        }

        public IPrimaryKeyAttribute PrimaryKeyAttribute
        {
            get
            {
                return _primaryKeyAttribute;
            }
            set
            {
                _primaryKeyAttribute = value;
            }
        }

        public List<IReferenceAttribute> ReferenceAttributes
        {
            get
            {
                return _referenceAttributes;
            }
            set
            {
                _referenceAttributes = value;
            }
        }

        public int Rank
        {
            get
            {
                return _rank;
            }
            set
            {
                _rank = value;
            }
        }

        //Methods
        public virtual void Reset()
        {
            _name = string.Empty;
            _fullName = string.Empty;
            _isUnderlying = false;
            _underlyingName = string.Empty;
            _underlyingType = null;
            _keyType = null;
            _persistType = null;

            _hasObjects = false;
            _isView = false;
            _hasPrimaryKey = false;
            _hasCompoundKeys = false;
            _hasForeignKeys = false;
            _hasBinaryProperties = false;

            _primaryKey = null;
            _properties = null;
            _foreignKeys = null;
            _compoundKeys = null;
            _valueProperties = null;
            _referenceProperties = null;
            _binaryProperties = null;
            _indexes = null;

            _indexAttributes = null;
            _primaryKeyAttribute = null;
            _referenceAttributes = null;
        }

        public virtual void Parse(Type type)
        {
            //Set the class level values
            PersistType = type;
            FullName = type.FullName;
            Name = type.Name;
            IsView = typeof(IView).IsAssignableFrom(type);
            BaseType = GetBaseType(type);

            if (IsView) PersistType = Open.Data.Persistence.Common.GetPersistType(PersistType);

            _compoundList = new List<PropertyInfo>();
            _foreignList = new List<PropertyInfo>();
            _indexes = new List<PropertyInfo[]>();
            _indexAttributes = new List<IIndexAttribute>();
            _referenceAttributes = new List<IReferenceAttribute>();

            //Get the key property values
            GetKeyProperties(PersistType);

            _propertyList = new List<PropertyInfo>();
            _valueList = new List<PropertyInfo>();
            _referenceList = new List<PropertyInfo>();
            _binaryList = new List<PropertyInfo>();
                
            //Get the properties that arent primary or compound keys
            if (IsView)
            {
                ConvertKeyProperties(type); //Make sure the view defines the primary or compound keys
                GetViewProperties(type);
            }
            else
            {
                GetProperties(type);
            }

            //Set the property arrays
            Properties = _propertyList.ToArray();
            ValueProperties = _valueList.ToArray();
            ReferenceProperties = _referenceList.ToArray();
            BinaryProperties = _binaryList.ToArray();

            if (_compoundList.Count > 0) CompoundKeys = _compoundList.ToArray();
            if (_foreignList.Count > 0) ForeignKeys = _foreignList.ToArray();

            _propertyList = null;
            _valueList = null;
            _referenceList = null;
            _binaryList = null;
            _compoundList = null;
            _foreignList = null;

            //Determine if this type is also the base type
            IsUnderlying = (type == UnderlyingType || IsView);

            //Get the rank
            Rank = GetRank(type);
        }

        public virtual void Validate(Type type)
        {
            if (type.IsAbstract) throw new PersistenceException("Abstract type " + type.FullName + " cannot be persisted directly.");

            //Validate that there is a primary key
            if (!IsView && !HasPrimaryKey && !HasCompoundKeys) throw new PersistenceException("Type " + type.FullName + " does not have a primary key defined.");

            if (HasObjects)
            {
                foreach (PropertyInfo prop in ReferenceProperties)
                {
                    if (prop.PropertyType.Equals(type)) throw new PersistenceException("Type " + type.FullName + " cannot have recursive reference properties.");

                    //Check that the referenced object has a primary key
                    ReflectionParser parser = new ReflectionParser();
                    parser.Parse(prop.PropertyType);
                    if (!parser.HasPrimaryKey) throw new PersistenceException("Property Type " + prop.PropertyType + " cannot reference a type that doesnt have a single primary key.");
                }
            }
        }

        protected static bool IsUnderlyingBase(Type type)
        {
            //Null
            if (type.BaseType == null) return true;

            //Object or other base class
            if (!typeof(IPersistable).IsAssignableFrom(type.BaseType)) return true;

            //View
            if (type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeof(View<>)) return true;

            return false;
        }

        //Return true if type is a base type or is an abstract base type
        protected static bool IsBaseAbstract(Type type)
        {
            if (IsUnderlyingBase(type)) return true;
            if (type.BaseType.IsAbstract) return IsBaseAbstract(type.BaseType);
            return false;
        }

        protected static Type GetBaseType(Type type)
        {
            if (IsUnderlyingBase(type)) return null;
            if (type.BaseType.IsAbstract) return GetBaseType(type.BaseType);
            return type.BaseType;
        }

        protected static int GetRank(Type type)
        {
            Type baseType = GetBaseType(type);

            if (baseType == null) return 0;

            return GetRank(baseType) + 1;
        }

        #endregion

        #region Implementation

        //Loads primary or compound key information recursively
        protected virtual void GetKeyProperties(Type type)
        {
            //Get only the properties for this type.
            PropertyInfo[] propertyInfo = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.GetProperty);
            object[] attributes = null;
            
            //Get pre recursive properties
            if (!type.IsAbstract)
            {
                foreach (PropertyInfo prop in propertyInfo)
                {
                    //Loop through attributes and get settings
                    attributes = prop.GetCustomAttributes(false);
                    foreach (Attribute attr in attributes)
                    {
                        if (attr is ColumnAttribute)
                        {
                            //Set to the base type for now, since at least one property is persisted
                            //it may be reset in a lower recursion
                            UnderlyingType = type;
                            UnderlyingName = type.Name;
                            break;
                        }
                    }
                }
            }

            //Load any base class properties if the base is not of type object
            if (!IsUnderlyingBase(type)) GetKeyProperties(type.BaseType);

            //Get post recursive properties (attributes override those in lower classes)
            //All keys should be checked here
            foreach (PropertyInfo prop in propertyInfo)
            {
                //Loop through attributes and get settings
                attributes = prop.GetCustomAttributes(false);
                foreach (Attribute attr in attributes)
                {
                    if (attr is ColumnAttribute)
                    {
                        //Add to PropertyInfo array if valid
                        if (!Common.IsAllowedType(prop.PropertyType)) throw new PersistenceException("Type " + prop.PropertyType.ToString() + " is not an allowed persistable type.");

                        if (attr is PrimaryKeyAttribute)
                        {
                            if (typeof(IPersistable).IsAssignableFrom(prop.PropertyType)) throw new PersistenceException("Object properties may not be declared as primary keys for Type " + type.FullName + ".");
                            if (HasPrimaryKey) throw new PersistenceException("A primary key has already been declared for Type " + type.FullName + ".");
                            if (HasCompoundKeys) throw new PersistenceException("A primary key may not be declared with a compound key for Type " + type.FullName + ".");
                            HasPrimaryKey = true;
                            PrimaryKey = prop;
                            PrimaryKeyAttribute = attr as PrimaryKeyAttribute;
                            KeyType = type;
                        }
                        else if (attr is ForeignKeyAttribute)
                        {
                            if (typeof(IPersistable).IsAssignableFrom(prop.PropertyType)) throw new PersistenceException("Foreign key attributes may not be declared for object properties for Type " + type.FullName + " as they are already implicitly defined.");
                            HasForeignKeys = true;

                            _foreignList.Add(prop);
                            _referenceAttributes.Add(attr as ForeignKeyAttribute);
                        }
                    }
                }
            }

            //Calculate class level attributes
            attributes = type.GetCustomAttributes(false);

            foreach (Attribute attr in attributes)
            {
                if (attr is ColumnsAttribute)
                {
                    if (attr is PrimaryKeysAttribute)
                    {
                        if (HasPrimaryKey) throw new PersistenceException("A primary key has already been declared for Type " + type.FullName + ".");
                        if (HasCompoundKeys) throw new PersistenceException("A compound primary key has already been declared for Type " + type.FullName + ".");
                        
                        PrimaryKeysAttribute keys = attr as PrimaryKeysAttribute;
                        Columns columns = new Columns(type, keys.Columns);

                        //Set up primary key values
                        if (columns.Count == 1)
                        {
                            if (typeof(IPersistable).IsAssignableFrom(columns[0].PropertyInfo.PropertyType)) throw new PersistenceException("Object properties may not be declared as primary keys for Type " + type.FullName + ".");
                            HasPrimaryKey = true;
                            PrimaryKey = columns[0].PropertyInfo;
                        }
                        else
                        {
                            HasCompoundKeys = true;
                            foreach (Column column in columns)
                            {
                                if (typeof(IPersistable).IsAssignableFrom(column.PropertyInfo.PropertyType)) throw new PersistenceException("Object properties may not be declared as primary keys for Type " + type.FullName + ".");
                                _compoundList.Add(column.PropertyInfo);
                            }
                        }

                        KeyType = type;
                        PrimaryKeyAttribute = keys;
                    }
                }
            }
        }

        protected virtual void GetProperties(Type type)
        {
            PropertyInfo[] propertyInfo = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.GetProperty);
            object[] attributes = null;

            //Get pre recursive properties
            foreach (PropertyInfo prop in propertyInfo)
            {
                //Loop through attributes and get settings
                attributes = prop.GetCustomAttributes(false);

                bool isColumn = false;

                foreach (Attribute attr in attributes)
                {
                    if (attr is ColumnAttribute)
                    {
                        isColumn = true;
                        
                        if (attr is IndexAttribute)
                        {
                            Indexes.Add(new PropertyInfo[] { prop });
                            IndexAttributes.Add(attr as IndexAttribute);
                        }
                    }
                }

                //Only use normal columns
                if (isColumn && PrimaryKey != prop && !_compoundList.Contains(prop))
                {
                    //Add to PropertyInfo array if valid
                    if (!Common.IsAllowedType(prop.PropertyType)) throw new PersistenceException("Type " + prop.PropertyType.ToString() + " is not an allowed persistable type for class " + type.ToString());

                    //Add to all properties                            
                    _propertyList.Add(prop);

                    //Determine if value or object property
                    if (typeof(IPersistable).IsAssignableFrom(prop.PropertyType))
                    {
                        HasObjects = true;

                        _referenceList.Add(prop);
                    }
                    else
                    {
                        _valueList.Add(prop);
                    }

                    if (typeof(Stream).IsAssignableFrom(prop.PropertyType))
                    {
                        HasBinaryProperties = true;
                        _binaryList.Add(prop);
                    }
                }
            }

            //class level columns attributes
            //Calculate class level attributes
            attributes = type.GetCustomAttributes(false);

            foreach (Attribute attr in attributes)
            {
                if (attr is ColumnsAttribute)
                {
                    if (attr is IndexColumnsAttribute)
                    {
                        IndexColumnsAttribute indexColumns = attr as IndexColumnsAttribute;
                        Columns columns = new Columns(type, indexColumns.Columns);
                        Indexes.Add(columns.ToPropertyInfoArray());
                        IndexAttributes.Add(indexColumns);
                    }
                }
            }

            if (!IsUnderlyingBase(type) && type.BaseType.IsAbstract) GetProperties(type.BaseType);
        }

        //Convert the key properties to the view type from the persist type
        private void ConvertKeyProperties(Type type)
        {
            if (HasPrimaryKey)
            {
                string name = PrimaryKey.Name;
                PrimaryKey = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.GetProperty);
                if (PrimaryKey == null) throw new PersistenceException("The primary key field " + name + " was not found in the " + type.ToString() + " definition. This property is required.");

                KeyType = type;
            }
            else if (HasCompoundKeys)
            {
                //Copy the list from the original type, replacing with the 
                List<PropertyInfo> list = new List<PropertyInfo>(_compoundList);
                _compoundList.Clear();

                foreach (PropertyInfo prop in list)
                {
                    string name = prop.Name;
                    PropertyInfo compound = type.GetProperty(name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.GetProperty);

                    if (compound == null) throw new PersistenceException("The compound key field " + name + " was not found in the " + type.ToString() + " definition. This property is required.");

                    _compoundList.Add(compound);
                }

                KeyType = type;
            }
        }

        //Get view properties that match to properties in the persisted type
        private void GetViewProperties(Type type)
        {
            PropertyInfo[] propertyInfo = type.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.GetProperty);

            //Get all properties found in the persisted type
            foreach (PropertyInfo prop in propertyInfo)
            {
                if (PersistType.GetProperty(prop.Name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.GetProperty) != null)
                {
                    _propertyList.Add(prop);

                    //Determine if value or object property
                    if (typeof(IPersistable).IsAssignableFrom(prop.PropertyType))
                    {
                        HasObjects = true;

                        _referenceList.Add(prop);
                    }
                    else
                    {
                        _valueList.Add(prop);
                    }

                    if (typeof(Stream).IsAssignableFrom(prop.PropertyType))
                    {
                        HasBinaryProperties = true;
                        _binaryList.Add(prop);
                    }
                }
            }
        }

        #endregion
    }
}
